// Copyright 2003-2005 Arthur van Hoff, Rick Blair
// Licensed under Apache License version 2.0
// Original license LGPL
package javax.jmdns.impl.tasks;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;

import javax.jmdns.impl.DNSConstants;
import javax.jmdns.impl.DNSOutgoing;
import javax.jmdns.impl.DNSQuestion;
import javax.jmdns.impl.DNSRecord;
import javax.jmdns.impl.DNSState;
import javax.jmdns.impl.JmDNSImpl;
import javax.jmdns.impl.ServiceInfoImpl;

/** The Prober sends three consecutive probes for all service infos that needs
 * probing as well as for the host name. The state of each service info of the
 * host name is advanced, when a probe has been sent for it. When the prober has
 * run three times, it launches an Announcer.
 * <p/>
 * If a conflict during probes occurs, the affected service infos (and affected
 * host name) are taken away from the prober. This eventually causes the prober
 * tho cancel itself. */
public class Prober extends TimerTask {
	/**
     * 
     */
	private final JmDNSImpl jmDNSImpl;
	/** The state of the prober. */
	DNSState taskState = DNSState.PROBING_1;

	@SuppressWarnings("rawtypes")
	public Prober(JmDNSImpl jmDNSImpl) {
		this.jmDNSImpl = jmDNSImpl;
		// Associate the host name to this, if it needs probing
		if (this.jmDNSImpl.getState() == DNSState.PROBING_1) {
			this.jmDNSImpl.setTask(this);
		}
		// Associate services to this, if they need probing
		synchronized (this.jmDNSImpl) {
			for (Iterator iterator = this.jmDNSImpl.getServices().values()
					.iterator(); iterator.hasNext();) {
				ServiceInfoImpl info = (ServiceInfoImpl) iterator.next();
				if (info.getState() == DNSState.PROBING_1) {
					info.setTask(this);
				}
			}
		}
	}

	public void start(Timer timer) {
		long now = System.currentTimeMillis();
		if (now - this.jmDNSImpl.getLastThrottleIncrement() < DNSConstants.PROBE_THROTTLE_COUNT_INTERVAL) {
			this.jmDNSImpl.setThrottle(this.jmDNSImpl.getThrottle() + 1);
		} else {
			this.jmDNSImpl.setThrottle(1);
		}
		this.jmDNSImpl.setLastThrottleIncrement(now);
		if (this.jmDNSImpl.getState() == DNSState.ANNOUNCED
				&& this.jmDNSImpl.getThrottle() < DNSConstants.PROBE_THROTTLE_COUNT) {
			timer.schedule(
					this,
					JmDNSImpl.getRandom().nextInt(
							1 + DNSConstants.PROBE_WAIT_INTERVAL),
					DNSConstants.PROBE_WAIT_INTERVAL);
		} else {
			timer.schedule(this, DNSConstants.PROBE_CONFLICT_INTERVAL,
					DNSConstants.PROBE_CONFLICT_INTERVAL);
		}
	}

	@SuppressWarnings("rawtypes")
	public boolean cancel() {
		// Remove association from host name to this
		if (this.jmDNSImpl.getTask() == this) {
			this.jmDNSImpl.setTask(null);
		}
		// Remove associations from services to this
		synchronized (this.jmDNSImpl) {
			for (Iterator i = this.jmDNSImpl.getServices().values().iterator(); i
					.hasNext();) {
				ServiceInfoImpl info = (ServiceInfoImpl) i.next();
				if (info.getTask() == this) {
					info.setTask(null);
				}
			}
		}
		return super.cancel();
	}

	@SuppressWarnings({ "rawtypes", "unchecked" })
	public void run() {
		synchronized (this.jmDNSImpl.getIoLock()) {
			DNSOutgoing out = null;
			try {
				// send probes for JmDNS itself
				if (this.jmDNSImpl.getState() == taskState
						&& this.jmDNSImpl.getTask() == this) {
					if (out == null) {
						out = new DNSOutgoing(DNSConstants.FLAGS_QR_QUERY);
					}
					out.addQuestion(new DNSQuestion(this.jmDNSImpl
							.getLocalHost().getName(), DNSConstants.TYPE_ANY,
							DNSConstants.CLASS_IN));
					this.jmDNSImpl.getLocalHost().addAddressRecords(out, true);
					this.jmDNSImpl.advanceState();
				}
				// send probes for services
				// Defensively copy the services into a local list,
				// to prevent race conditions with methods registerService
				// and unregisterService.
				List list;
				synchronized (this.jmDNSImpl) {
					list = new LinkedList(this.jmDNSImpl.getServices().values());
				}
				for (Iterator i = list.iterator(); i.hasNext();) {
					ServiceInfoImpl info = (ServiceInfoImpl) i.next();
					synchronized (info) {
						if (info.getState() == taskState
								&& info.getTask() == this) {
							info.advanceState();
							if (out == null) {
								out = new DNSOutgoing(
										DNSConstants.FLAGS_QR_QUERY);
								out.addQuestion(new DNSQuestion(info
										.getQualifiedName(),
										DNSConstants.TYPE_ANY,
										DNSConstants.CLASS_IN));
							}
							// the "unique" flag should be not set here because these
							// answers haven't been proven unique yet
							// this means the record will not exactly match the
							// announcement record
							out.addAuthorativeAnswer(new DNSRecord.Service(info
									.getQualifiedName(), DNSConstants.TYPE_SRV,
									DNSConstants.CLASS_IN,
									DNSConstants.DNS_TTL, info.getPriority(),
									info.getWeight(), info.getPort(),
									this.jmDNSImpl.getLocalHost().getName()));
						}
					}
				}
				if (out != null) {
					this.jmDNSImpl.send(out);
				} else {
					// If we have nothing to send, another timer taskState ahead
					// of us has done the job for us. We can cancel.
					cancel();
					return;
				}
			} catch (Throwable e) {
				this.jmDNSImpl.recover();
			}
			taskState = taskState.advance();
			if (!taskState.isProbing()) {
				cancel();
				this.jmDNSImpl.startAnnouncer();
			}
		}
	}
}